using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using WeatherSystem.API.Data;
using WeatherSystem.API.Models;
using WeatherSystem.API.Services;
using WeatherSystem.API.Specifications;

namespace WeatherSystem.API.Repositories
{
    /// <summary>
    /// Base read-only repository implementation
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ReadRepository<T> : IReadRepository<T> where T : class
    {
        protected readonly WeatherDbContext _context;
        protected readonly DbSet<T> _dbSet;
        protected readonly ILogger<ReadRepository<T>> _logger;

        public ReadRepository(WeatherDbContext context, ILogger<ReadRepository<T>> logger)
        {
            _context = context;
            _dbSet = context.Set<T>();
            _logger = logger;
        }

        public virtual async Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
        {
            return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
        }

        public virtual async Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
        {
            return await _dbSet.AsNoTracking().ToListAsync(cancellationToken);
        }

        public virtual async Task<IEnumerable<T>> GetBySpecificationAsync(ISpecification<T> specification, CancellationToken cancellationToken = default)
        {
            return await ApplySpecification(specification).ToListAsync(cancellationToken);
        }

        public virtual async Task<T?> GetSingleBySpecificationAsync(ISpecification<T> specification, CancellationToken cancellationToken = default)
        {
            return await ApplySpecification(specification).FirstOrDefaultAsync(cancellationToken);
        }

        public virtual async Task<int> CountAsync(ISpecification<T>? specification = null, CancellationToken cancellationToken = default)
        {
            if (specification == null)
                return await _dbSet.CountAsync(cancellationToken);

            return await ApplySpecification(specification).CountAsync(cancellationToken);
        }

        public virtual async Task<bool> AnyAsync(ISpecification<T>? specification = null, CancellationToken cancellationToken = default)
        {
            if (specification == null)
                return await _dbSet.AnyAsync(cancellationToken);

            return await ApplySpecification(specification).AnyAsync(cancellationToken);
        }

        public virtual async Task<PagedResult<T>> GetPagedAsync(int page, int pageSize, ISpecification<T>? specification = null, CancellationToken cancellationToken = default)
        {
            var query = specification == null ? _dbSet.AsNoTracking() : ApplySpecification(specification);
            
            var totalCount = await query.CountAsync(cancellationToken);
            var items = await query
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync(cancellationToken);

            return new PagedResult<T>(items, totalCount, page, pageSize);
        }

        public virtual async Task<IEnumerable<TResult>> GetProjectedAsync<TResult>(Expression<Func<T, TResult>> projection, ISpecification<T>? specification = null, CancellationToken cancellationToken = default)
        {
            var query = specification == null ? _dbSet.AsNoTracking() : ApplySpecification(specification);
            return await query.Select(projection).ToListAsync(cancellationToken);
        }

        public virtual async Task<PagedResult<TResult>> GetProjectedPagedAsync<TResult>(Expression<Func<T, TResult>> projection, int page, int pageSize, ISpecification<T>? specification = null, CancellationToken cancellationToken = default)
        {
            var query = specification == null ? _dbSet.AsNoTracking() : ApplySpecification(specification);
            
            var totalCount = await query.CountAsync(cancellationToken);
            var items = await query
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .Select(projection)
                .ToListAsync(cancellationToken);

            return new PagedResult<TResult>(items, totalCount, page, pageSize);
        }

        protected virtual IQueryable<T> ApplySpecification(ISpecification<T> specification)
        {
            var query = _dbSet.AsQueryable();

            if (specification.Criteria != null)
                query = query.Where(specification.Criteria);

            // Apply includes
            query = specification.Includes.Aggregate(query, (current, include) => current.Include(include));
            query = specification.IncludeStrings.Aggregate(query, (current, include) => current.Include(include));

            // Apply ordering
            if (specification.OrderBy != null)
                query = query.OrderBy(specification.OrderBy);
            else if (specification.OrderByDescending != null)
                query = query.OrderByDescending(specification.OrderByDescending);

            // Apply grouping
            if (specification.GroupBy != null)
                query = query.GroupBy(specification.GroupBy).SelectMany(x => x);

            // Apply paging
            if (specification.IsPagingEnabled)
                query = query.Skip(specification.Skip).Take(specification.Take);

            return query.AsNoTracking();
        }
    }

    /// <summary>
    /// Base write repository implementation
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class WriteRepository<T> : IWriteRepository<T> where T : class
    {
        protected readonly WeatherDbContext _context;
        protected readonly DbSet<T> _dbSet;
        protected readonly ILogger<WriteRepository<T>> _logger;

        public WriteRepository(WeatherDbContext context, ILogger<WriteRepository<T>> logger)
        {
            _context = context;
            _dbSet = context.Set<T>();
            _logger = logger;
        }

        public virtual async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
        {
            await _dbSet.AddAsync(entity, cancellationToken);
            return entity;
        }

        public virtual async Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
        {
            var entityList = entities.ToList();
            await _dbSet.AddRangeAsync(entityList, cancellationToken);
            return entityList;
        }

        public virtual Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
        {
            _dbSet.Update(entity);
            return Task.CompletedTask;
        }

        public virtual Task UpdateRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
        {
            _dbSet.UpdateRange(entities);
            return Task.CompletedTask;
        }

        public virtual Task DeleteAsync(T entity, CancellationToken cancellationToken = default)
        {
            _dbSet.Remove(entity);
            return Task.CompletedTask;
        }

        public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
        {
            var entity = await _dbSet.FindAsync(new object[] { id }, cancellationToken);
            if (entity != null)
                _dbSet.Remove(entity);
        }

        public virtual Task DeleteRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
        {
            _dbSet.RemoveRange(entities);
            return Task.CompletedTask;
        }

        public virtual async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            return await _context.SaveChangesAsync(cancellationToken);
        }
    }

    /// <summary>
    /// Weather data read repository implementation
    /// </summary>
    public class WeatherDataReadRepository : ReadRepository<WeatherDataEntry>, IWeatherDataReadRepository
    {
        private readonly ICacheService _cacheService;

        public WeatherDataReadRepository(WeatherDbContext context, ILogger<WeatherDataReadRepository> logger, ICacheService cacheService) 
            : base(context, logger)
        {
            _cacheService = cacheService;
        }

        public async Task<WeatherDataEntry?> GetLatestByCityAsync(Guid cityId, CancellationToken cancellationToken = default)
        {
            var cacheKey = $"weather:latest:{cityId}";
            return await _cacheService.GetOrSetAsync(cacheKey, async () =>
            {
                var spec = new LatestWeatherByCitySpecification(cityId);
                return await GetSingleBySpecificationAsync(spec, cancellationToken);
            }, TimeSpan.FromMinutes(5));
        }

        public async Task<IEnumerable<WeatherDataEntry>> GetHistoryAsync(Guid cityId, DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
        {
            var cacheKey = $"weather:history:{cityId}:{startDate:yyyyMMdd}:{endDate:yyyyMMdd}";
            return await _cacheService.GetOrSetAsync(cacheKey, async () =>
            {
                var spec = new WeatherDataWithCitySpecification(cityId, startDate, endDate);
                return await GetBySpecificationAsync(spec, cancellationToken);
            }, TimeSpan.FromMinutes(15));
        }

        public async Task<PagedResult<Dictionary<string, object>>> GetWeatherDataPagedAsync(
            int page, int pageSize, Guid? cityId = null, DateTime? startDate = null, DateTime? endDate = null, 
            string? source = null, string? sortBy = null, string? sortDirection = null, CancellationToken cancellationToken = default)
        {
            var spec = new WeatherDataWithCitySpecification(cityId, startDate, endDate, source);
            spec.ApplyPaging((page - 1) * pageSize, pageSize);

            // Apply sorting
            if (sortBy?.ToLower() == "city")
            {
                if (sortDirection?.ToLower() == "asc")
                    spec.ApplyOrderBy(w => w.City!.Name);
                else
                    spec.ApplyOrderByDescending(w => w.City!.Name);
            }

            var entities = await GetBySpecificationAsync(spec, cancellationToken);
            var totalCount = await CountAsync(new WeatherDataWithCitySpecification(cityId, startDate, endDate, source), cancellationToken);

            var dictionaries = entities.Select(entity => ConvertToDictionary(entity)).ToList();
            return new PagedResult<Dictionary<string, object>>(dictionaries, totalCount, page, pageSize);
        }

        public async Task<IEnumerable<Dictionary<string, object>>> GetWeatherDataAsDictionaryAsync(Expression<Func<WeatherDataEntry, bool>>? predicate = null, CancellationToken cancellationToken = default)
        {
            var query = _dbSet
                .Include(e => e.City)
                .Include(e => e.Metrics)
                .ThenInclude(m => m.Definition)
                .AsNoTracking();

            if (predicate != null)
                query = query.Where(predicate);

            var entities = await query.ToListAsync(cancellationToken);
            return entities.Select(ConvertToDictionary);
        }

        private static Dictionary<string, object> ConvertToDictionary(WeatherDataEntry entity)
        {
            var result = new Dictionary<string, object>
            {
                ["id"] = entity.Id,
                ["cityId"] = entity.CityId,
                ["cityName"] = entity.City?.Name ?? "",
                ["recordedAt"] = entity.RecordedAt,
                ["source"] = entity.Source ?? "",
                ["createdAt"] = entity.CreatedAt
            };

            foreach (var metric in entity.Metrics)
            {
                result[metric.MetricKey] = ConvertRawValueToTyped(metric.RawValue, metric.DataType);
            }

            return result;
        }

        private static object ConvertRawValueToTyped(string rawValue, string dataType)
        {
            return dataType.ToLower() switch
            {
                "decimal" => decimal.TryParse(rawValue, out var d) ? d : 0m,
                "int" => int.TryParse(rawValue, out var i) ? i : 0,
                "boolean" => bool.TryParse(rawValue, out var b) && b,
                _ => rawValue
            };
        }
    }

    /// <summary>
    /// Weather data write repository implementation
    /// </summary>
    public class WeatherDataWriteRepository : WriteRepository<WeatherDataEntry>, IWeatherDataWriteRepository
    {
        private readonly ICacheInvalidationService _cacheInvalidation;

        public WeatherDataWriteRepository(
            WeatherDbContext context, 
            ILogger<WeatherDataWriteRepository> logger,
            ICacheInvalidationService cacheInvalidation) 
            : base(context, logger)
        {
            _cacheInvalidation = cacheInvalidation;
        }

        public async Task<List<Guid>> CreateBatchAsync(List<CreateDynamicWeatherDataRequest> requests, CancellationToken cancellationToken = default)
        {
            var results = new List<Guid>();
            var cityIds = new HashSet<Guid>();

            foreach (var request in requests)
            {
                var weatherDataEntry = new WeatherDataEntry
                {
                    CityId = request.CityId,
                    RecordedAt = request.RecordedAt,
                    Source = request.Source ?? "Batch"
                };

                var metricValues = new List<MetricValue>();
                if (request.Metrics != null)
                {
                    var validMetrics = await _context.MetricDefinitions
                        .Where(m => m.IsActive)
                        .AsNoTracking()
                        .ToListAsync(cancellationToken);

                    foreach (var kvp in request.Metrics)
                    {
                        var metricDef = validMetrics.FirstOrDefault(m => m.Key == kvp.Key);
                        if (metricDef != null)
                        {
                            metricValues.Add(new MetricValue
                            {
                                WeatherDataEntryId = weatherDataEntry.Id,
                                MetricKey = kvp.Key,
                                RawValue = kvp.Value?.ToString() ?? string.Empty,
                                DataType = metricDef.DataType
                            });
                        }
                    }
                }

                weatherDataEntry.Metrics = metricValues;
                await AddAsync(weatherDataEntry, cancellationToken);
                results.Add(weatherDataEntry.Id);
                cityIds.Add(request.CityId);
            }

            await SaveChangesAsync(cancellationToken);

            // Invalidate cache for affected cities
            foreach (var cityId in cityIds)
            {
                await InvalidateCacheAsync(cityId, cancellationToken);
            }

            return results;
        }

        public async Task<bool> UpdateWeatherDataAsync(Guid id, UpdateDynamicWeatherDataRequest request, CancellationToken cancellationToken = default)
        {
            var entry = await _context.WeatherDataEntries
                .Include(e => e.Metrics)
                .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);

            if (entry == null)
                return false;

            // Update basic information
            if (request.CityId.HasValue)
                entry.CityId = request.CityId.Value;
            if (request.RecordedAt.HasValue)
                entry.RecordedAt = request.RecordedAt.Value;
            if (!string.IsNullOrEmpty(request.Source))
                entry.Source = request.Source;

            // Update metric values
            if (request.Metrics != null)
            {
                var validMetrics = await _context.MetricDefinitions
                    .Where(m => m.IsActive)
                    .AsNoTracking()
                    .ToListAsync(cancellationToken);

                foreach (var kvp in request.Metrics)
                {
                    var existingMetric = entry.Metrics.FirstOrDefault(m => m.MetricKey == kvp.Key);
                    var metricDef = validMetrics.FirstOrDefault(m => m.Key == kvp.Key);

                    if (metricDef != null)
                    {
                        if (existingMetric != null)
                        {
                            existingMetric.RawValue = kvp.Value?.ToString() ?? string.Empty;
                            existingMetric.DataType = metricDef.DataType;
                        }
                        else
                        {
                            entry.Metrics.Add(new MetricValue
                            {
                                WeatherDataEntryId = entry.Id,
                                MetricKey = kvp.Key,
                                RawValue = kvp.Value?.ToString() ?? string.Empty,
                                DataType = metricDef.DataType
                            });
                        }
                    }
                }
            }

            await UpdateAsync(entry, cancellationToken);
            await SaveChangesAsync(cancellationToken);
            await InvalidateCacheAsync(entry.CityId, cancellationToken);

            return true;
        }

        public async Task InvalidateCacheAsync(Guid cityId, CancellationToken cancellationToken = default)
        {
            await _cacheInvalidation.InvalidateWeatherDataAsync(cityId);
        }

        public override async Task<WeatherDataEntry> AddAsync(WeatherDataEntry entity, CancellationToken cancellationToken = default)
        {
            var result = await base.AddAsync(entity, cancellationToken);
            await InvalidateCacheAsync(entity.CityId, cancellationToken);
            return result;
        }

        public override async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
        {
            var entity = await _context.WeatherDataEntries
                .AsNoTracking()
                .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
            
            if (entity != null)
            {
                await base.DeleteAsync(id, cancellationToken);
                await InvalidateCacheAsync(entity.CityId, cancellationToken);
            }
        }
    }
}